package com.beeweeb.services.tps.mbean;

import com.beeweeb.services.tps.JobInvoker;
import com.beeweeb.services.tps.clustering.UdpClusterChannel;
import com.beeweeb.services.tps.concurrent.UnboundedThreadPoolExecutor;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.jboss.system.ServiceMBean;
import org.jgroups.Address;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;

/**
 * @author Guido D'Albore
 */
public class ThreadPoolScheduler extends ThreadPoolSchedulerSupport implements ThreadPoolSchedulerMBean {

    public static final Date NO_EXPIRE = TriggerUtils.getDateOf(0, 0, 0, 31, 12, 2099);
    Scheduler scheduler;
    JobDetail job;
    Trigger trigger;
    UnboundedThreadPoolExecutor threadPool;
    String cronExpression;
    UdpClusterChannel cluster;
    boolean isClusteringEnabled;
    String clusterMulticastIpAddress;
    String clusterName;
    boolean notifyJobsCompleted;

    public ThreadPoolScheduler() {
        super();
    }

    @Override
    public void configurationUpdated() {
        if (getState() == ServiceMBean.STARTED) {
            startScheduler();
        }
    }

    @Override
    public String getNextFireTime() {
        if (isSchedulerRunning()) {
            try {
                Trigger t = scheduler.getTrigger(Scheduler.DEFAULT_GROUP, "main-trigger");

                if (t.getNextFireTime() == null) {
                    return "No more triggering dates.";
                } else {
                    //logger.debug("Next triggering: {}", sdf.format(t.getNextFireTime()));
                    //return sdf.format(trigger.getNextFireTime());
                    logger.debug("Next triggering: {}", sdf.format(t.getFireTimeAfter(new Date())));
                    return sdf.format(trigger.getFireTimeAfter(new Date()));

                }

            } catch (Exception ex) {
            }
        }

        return "Quartz scheduler not started.";
    }

    private void initializeScheduler() {
        try {
            StdSchedulerFactory sf = new StdSchedulerFactory();

            Properties p = new Properties();

            p.setProperty("org.quartz.scheduler.instanceName", (jobInstanceName != null && !jobInstanceName.isEmpty()) ? jobInstanceName : "QuartzThreadPoolScheduler");
            p.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
            p.setProperty("org.quartz.threadPool.threadCount", String.valueOf(1));
            p.setProperty("org.quartz.threadPool.threadPriority", String.valueOf(Thread.NORM_PRIORITY));
            if (isInUserTransaction()) {
                p.setProperty("org.quartz.scheduler.wrapJobExecutionInUserTransaction", "true");
                p.setProperty("org.quartz.scheduler.userTransactionURL", "UserTransaction");
            }

            sf.initialize(p);
            scheduler = sf.getScheduler();
        } catch (Exception ex) {
            logger.error("Cannot initialize quartz scheduler.", ex);
            lastError = getStackTrace(ex);
        }
    }

    private void setupThreadPool() {
        threadPool = new UnboundedThreadPoolExecutor(threadPoolSize);
    }

    private void setupJob() {
        job = new JobDetail(Scheduler.DEFAULT_GROUP, "main-job", JobInvoker.class);

        HashMap jobParameters = new HashMap();
        jobParameters.put("ThreadPool", threadPool);

        Class clazz = findClass(jobPoolExecutorClass);
        jobParameters.put("JobPoolExecutorClass", clazz);

        jobParameters.put("jobPoolAcceptedCount", Integer.valueOf(jobPoolAcceptedCount));

        jobParameters.put("jobPoolExclusiveExecution", Boolean.valueOf(jobPoolExclusiveExecution));

        jobParameters.put("ThreadPoolSchedulerMBean", this);

        job.setJobDataMap(new JobDataMap(jobParameters));


    }

    private Class findClass(String clazz) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class result = null;

        if (classLoader != null) {
            try {
                result = classLoader.loadClass(clazz);
            } catch (Exception ex) {
            }
        }
        if (result == null) {
            try {
                result = ThreadPoolScheduler.class.forName(clazz);
            } catch (Exception ex) {
            }
        }
        if (result == null) {
            try {
                result = ThreadPoolScheduler.class.getClassLoader().loadClass(clazz);
            } catch (Exception ex) {
            }

        }
        if (result == null) {
            try {
                result = ThreadPoolScheduler.class.getClassLoader().getParent().loadClass(clazz);
            } catch (Exception ex) {
            }

        }
        if (result == null) {
            try {
                result = ThreadPoolSchedulerClassLoader.getContextClassLoader().loadClass(clazz);
            } catch (Exception ex) {
            }
        }

        if (result == null) {
            String errorMessage = String.format("%s - Cannot find job pool executor class '%s' within the current ClassLoader.", sdf.format(new Date()), clazz);
            logger.error(errorMessage);
            setLastError(errorMessage);
        }
        return result;
    }

    private void setupTrigger() {
        // SETUP OPTIONAL PARAMETERS

        if (useCronTrigger) {
            try {
                trigger = new CronTrigger(Scheduler.DEFAULT_GROUP, "main-trigger", getCronExpression());
            } catch (Exception ex) {
                logger.error("Cannot setup quartz trigger. Cron expression not valid.", ex);
            }
        } else {
            Date triggerStartDate = TriggerUtils.getEvenSecondDate(new Date());
            Date triggerEndDate = NO_EXPIRE;
            int triggerRepeatCount = SimpleTrigger.REPEAT_INDEFINITELY;
            long triggerRepeatInterval = -1;

            if (repeatCount >= 0) {
                triggerRepeatCount = repeatCount;
            }

            if (repeatInterval >= 0) {
                triggerRepeatInterval = repeatInterval;
            }

            if (startDate != null) {
                triggerStartDate = startDate;
            }

            if (endDate != null) {
                triggerEndDate = endDate;
            }

            if (triggerRepeatInterval == -1) {
                // Create a SimpleTrigger that will occur at the given time, and not repeat.
                trigger = new SimpleTrigger(Scheduler.DEFAULT_GROUP, "main-trigger", triggerStartDate);
            } else {
                // Create a SimpleTrigger that will occur at the given time, and
                // repeat at the the given interval the given number of times,
                // or until the given end time
                trigger = new SimpleTrigger(Scheduler.DEFAULT_GROUP, "main-trigger", triggerStartDate, triggerEndDate, triggerRepeatCount, triggerRepeatInterval);
            }
        }
    }

    private void schedule() {
        try {
            scheduler.scheduleJob(job, trigger);
        } catch (Exception ex) {
            logger.error("Cannot schedule job.", ex);
            lastError = getStackTrace(ex);
        }

    }

    private void shutdownScheduler() {
        try {
            if (scheduler != null) {
                if (cluster != null) {
                    try {
                        // Leave the member from the cluster pool
                        cluster.disconnect();
                    } catch (Exception ex) {
                    }

                    try {
                        // Release all resources
                        cluster.close();
                    } catch (Exception ex) {
                    }

                    cluster = null;
                }

                // Halts the Scheduler's firing of Triggers, and cleans up all resources associated with the Scheduler.
                // The scheduler cannot be re-started.
                scheduler.shutdown();
                threadPool.shutdownNow();
            }
        } catch (Exception ex) {
            logger.error("Cannot safely shutdown the scheduler.", ex);
            lastError = getStackTrace(ex);
        }
    }

    private void startScheduler() {
        try {
            if (isClusteringEnabled) {
                try {
                    cluster = new UdpClusterChannel(this.clusterName, this.clusterMulticastIpAddress);
                    cluster.connect();
                } catch (Exception ex) {
                    logger.error("Cannot connect to cluster.", ex);
                    cluster = null;
                }
            }

            if (isSchedulerRunning()) {
                shutdownScheduler();
            }

            initializeScheduler();
            setupThreadPool();
            setupTrigger();
            setupJob();
            schedule();

            scheduler.start();
        } catch (Exception ex) {
            logger.error("Cannot initialize quartz scheduler.", ex);
            lastError = getStackTrace(ex);
        }
    }

    private void standbyScheduler() {
        try {
            scheduler.standby();
        } catch (Exception ex) {
            logger.error("Cannot put in standby quartz scheduler.", ex);
            lastError = getStackTrace(ex);
        }
    }

    public boolean isSchedulerRunning() {
        try {
            return (scheduler != null) && !scheduler.isInStandbyMode() && !scheduler.isShutdown();
        } catch (Exception ex) {
            lastError = getStackTrace(ex);
        }

        return false;
    }

    @Override
    public String getQuartzStatus() {
        try {
            if (scheduler == null) {
                return "Quartz not created. Invoke getLastError() method for details.";
            }

            if (scheduler.isShutdown()) {
                return "Quartz in shutdown.";
            }

            if (scheduler.isInStandbyMode()) {
                return "Quartz in stanby mode.";
            }

            return "Quartz started.";
        } catch (Exception ex) {
            lastError = getStackTrace(ex);
        }

        return "Quartz error. Invoke getLastError() method for details.";
    }

    @Override
    protected void startService() throws Exception {
        logger.info("Starting service...");

        super.startService();

        if (startQuartzAtStartup) {
            startScheduler();
        }

        logger.info("Starting service...done");
    }

    @Override
    protected void stopService() throws Exception {
        logger.info("Stopping service...");
        shutdownScheduler();
        super.stopService();
        logger.info("Stopping service...done");
    }

    public void restartQuartzScheduler() {
        startScheduler();
    }

    public void stopQuartzScheduler() {
        shutdownScheduler();
    }

    public int getEnqueuedThreadsCount() {
        if (threadPool != null) {
            return threadPool.getEnqueuedThreadsCount();
        }
        return 0;
    }

    public int getRunningThreadsCount() {
        if (threadPool != null) {
            return threadPool.getRunningThreadsCount();
        }
        return 0;
    }

    public long getThreadAverageProcessingTime() {
        if (threadPool != null) {
            return threadPool.getThreadAverageProcessingTime();
        }
        return 0;
    }

    public long getThreadFinishedCount() {
        if (threadPool != null) {
            return threadPool.getThreadFinishedCount();
        }
        return 0;
    }

    public long getThreadMaximumProcessingTime() {
        if (threadPool != null) {
            return threadPool.getThreadMaximumProcessingTime();
        }
        return 0;
    }

    public long getThreadMinimumProcessingTime() {
        if (threadPool != null) {
            return threadPool.getThreadMinimumProcessingTime();
        }
        return 0;
    }

    public long getThreadStartedCount() {
        if (threadPool != null) {
            return threadPool.getThreadStartedCount();
        }
        return 0;
    }

    public String displayThreadsQueue() {
        int count = 0;
        String result = "<empty queue>";

        if (threadPool != null) {
            for (Runnable r : threadPool.getThreadsInQueue()) {
                result += String.format("Thread#%d: %s\n", count, r.toString());
                count++;
            }
        }

        return result;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(String cronExpression) {
        // Check expression
        try {
            Trigger t = new CronTrigger(Scheduler.DEFAULT_GROUP, "main-trigger", cronExpression);
            this.cronExpression = cronExpression;
        } catch (Exception ex) {
            String message = String.format("%s - Cron expression not valid. See http://www.quartz-scheduler.org/docs/tutorials/crontrigger.html for details.", sdf.format(new Date()));
            logger.error(message, ex);
            setLastError(message);
        }
    }

    public String getClusterMembers() {
        String result = "";

        if (isClusterConnected()) {
            int i = 0;
            for (Address a : cluster.getView().getMembers()) {
                result += String.format("[%d] %s (%s);\n", i, a.toString(), ((i == 0) ? "active" : "passive"));
                i++;
            }
        } else {
            result = "Clustering disabled.";
        }

        return result;
    }

    public String getClusterMemberName() {
        if (isClusterConnected()) {
            return String.format("%s (%s)", cluster.getAddressAsString(), ((cluster.isFirstMember()) ? "active" : "passive"));
        } else {
            return "Clustering disabled.";
        }
    }

    public boolean isActiveClusterMember() {
        if (isClusterConnected()) {
            return cluster.isFirstMember();
        } else {
            return false;
        }
    }

    public boolean getUseClustering() {
        return isClusteringEnabled;
    }

    public void setUseClustering(boolean enabled) {
        this.isClusteringEnabled = enabled;
    }

    public boolean isClusterConnected() {
        return cluster != null;
    }

    @Override
    public String getClusterMulticastIpAddress() {
        return clusterMulticastIpAddress;
    }

    @Override
    public void setClusterMulticastIpAddress(String clusterMulticastIpAddress) {
        this.clusterMulticastIpAddress = clusterMulticastIpAddress;
        //TODO check input validity if clustering is enabled
//        try {
//            boolean isMulticast = InetAddress.getByName(this.clusterMulticastIpAddress).isMulticastAddress();
//            if(!isMulticast){
//                throw new IllegalArgumentException();
//            }
//        } catch (UnknownHostException ex) {
//            String message = this.clusterMulticastIpAddress+" - clusterMulticastIpAddress not valid. Please insert a valid ip address.";
//            logger.error(message, ex);
//            setLastError(message);
//        } catch(IllegalArgumentException e){
//            String message = this.clusterMulticastIpAddress+" - clusterMulticastIpAddress not valid. Please insert a valid multicast ip address.";
//            logger.error(message, e);
//            setLastError(message);
//        }
    }

    @Override
    public String getClusterName() {
        return this.clusterName;
    }

    @Override
    public void setClusterName(String clusterName) {
        this.clusterName = clusterName;
    }

    @Override
    public boolean isNotifyJobsCompleted() {
        return notifyJobsCompleted;
    }

    @Override
    public void setNotifyJobsCompleted(boolean notifyJobsCompleted) {
        this.notifyJobsCompleted = notifyJobsCompleted;
    }
}
